<script setup lang="tsx">
import {
  Attachments,
  type AttachmentsProps,
  Bubble,
  type BubbleListProps,
  Conversations,
  type ConversationsProps,
  Prompts,
  type PromptsProps,
  Sender,
  Welcome,
  useXAgent,
  useXChat, type ThoughtChainItem,
  XStream
} from 'ant-design-x-vue';
import {
  CloudUploadOutlined,
  CommentOutlined,
  EllipsisOutlined,
  FireOutlined,
  HeartOutlined,
  PaperClipOutlined,
  PlusOutlined,
  ReadOutlined,
  ShareAltOutlined,
  SmileOutlined,
} from '@ant-design/icons-vue';
import {Badge, Button, Space, theme} from 'ant-design-vue';
import {computed, ref, watch, type VNode} from 'vue';
import MarkdownIt from 'markdown-it';
import hljs from 'highlight.js';
import 'highlight.js/styles/default.css'; // 引入代码高亮的样式
import markdownItHighlightjs from 'markdown-it-highlightjs';

defineOptions({name: 'PlaygroundIndependent'});

const sleep = () => new Promise((resolve) => setTimeout(resolve, 500));

const renderTitle = (icon: VNode, title: string) => (
    <Space align="start">
      {icon}
      <span>{title}</span>
    </Space>
);

const defaultConversationsItems = [
  {
    key: '0',
    label: 'What is Ant Design X?',
  },
];

const placeholderPromptsItems: PromptsProps['items'] = [
  {
    key: '1',
    label: renderTitle(<FireOutlined style={{color: '#FF4D4F'}}/>, '热点话题'),
    description: 'What are you interested in?',
    children: [
      {
        key: '1-1',
        description: `什么是AI`,
      },
      {
        key: '1-2',
        description: `什么是大模型`,
      },
      {
        key: '1-3',
        description: `什么是Agent`,
      },
    ],
  },
  {
    key: '2',
    label: renderTitle(<ReadOutlined style={{color: '#1890FF'}}/>, 'Design Guide'),
    description: 'How to design a good product?',
    children: [
      {
        key: '2-1',
        icon: <HeartOutlined/>,
        description: `Know the well`,
      },
      {
        key: '2-2',
        icon: <SmileOutlined/>,
        description: `Set the AI role`,
      },
      {
        key: '2-3',
        icon: <CommentOutlined/>,
        description: `Express the feeling`,
      },
    ],
  },
];

const senderPromptsItems: PromptsProps['items'] = [
  {
    key: '1',
    description: 'Hot Topics',
    icon: <FireOutlined style={{color: '#FF4D4F'}}/>,
  },
  {
    key: '2',
    description: 'Design Guide',
    icon: <ReadOutlined style={{color: '#1890FF'}}/>,
  },
];

const roles: BubbleListProps['roles'] = {
  ai: {
    placement: 'start',
    typing: {step: 5, interval: 20},
    styles: {
      content: {
        borderRadius: '16px',
      },
    },
  },
  local: {
    placement: 'end',
    variant: 'shadow',
  },
};

// ==================== Style ====================
const {token} = theme.useToken();
const styles = computed(() => {
  return {
    layout: {
      width: '100%',
      'min-width': '1000px',
      height: '722px',
      'border-radius': `${token.value.borderRadius}px`,
      display: 'flex',
      background: `${token.value.colorBgContainer}`,
      'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`,
    },
    menu: {
      background: `${token.value.colorBgLayout}80`,
      width: '280px',
      height: '100%',
      display: 'flex',
      'flex-direction': 'column',
    },
    conversations: {
      padding: '0 12px',
      flex: 1,
      'overflow-y': 'auto',
    },
    chat: {
      height: '100%',
      width: '100%',
      'max-width': '700px',
      margin: '0 auto',
      'box-sizing': 'border-box',
      display: 'flex',
      'flex-direction': 'column',
      padding: `${token.value.paddingLG}px`,
      gap: '16px',
    },
    messages: {
      flex: 1,
    },
    placeholder: {
      'padding-top': '32px',
    },
    sender: {
      'box-shadow': token.value.boxShadow,
    },
    logo: {
      display: 'flex',
      height: '72px',
      'align-items': 'center',
      'justify-content': 'start',
      padding: '0 24px',
      'box-sizing': 'border-box',
    },
    'logo-img': {
      width: '24px',
      height: '24px',
      display: 'inline-block',
    },
    'logo-span': {
      display: 'inline-block',
      margin: '0 8px',
      'font-weight': 'bold',
      color: token.value.colorText,
      'font-size': '16px',
    },
    addBtn: {
      background: '#1677ff0f',
      border: '1px solid #1677ff34',
      width: 'calc(100% - 24px)',
      margin: '0 12px 24px 12px',
    }
  } as const
});

// ==================== State ====================
const headerOpen = ref(false);
const content = ref('');
const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems[0].key);
const attachedFiles = ref<AttachmentsProps['items']>([]);
const agentRequestLoading = ref(false);

// ==================== Runtime ====================
const OLLAMA_API_URL = 'http://192.168.3.209:11434/api/chat';
const MODEL = 'deepseek-r1:8b';

// 创建 MarkdownIt 实例并配置插件
const mdConfig = {
  html: true,
  xhtmlOut: true,
  breaks: true,
  langPrefix: "lang-",
  linkify: false,
  typographer: true,
  quotes: "“”‘’",
};

const md = new MarkdownIt(mdConfig)
    .use(markdownItHighlightjs, {
      highlightAuto: true,
      code: true,
      hljs: hljs
    });


// 将附件内容转换为文本
const convertAttachmentToText = async (file: File) => {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const text = event.target?.result as string;
      resolve(text);
    };
    reader.onerror = (error) => {
      reject(error);
    };
    reader.readAsText(file);
  });
};

// 将历史消息转换为文本
const convertMessagesToText = (messages: any[]) => {
  return messages.map((msg) => {
    const role = msg.status === 'local' ? 'user' : 'assistant';
    return `${role}: ${msg.message}`;
  }).join('\n');
};

let abortController = new AbortController();
const [agent] = useXAgent({
  request: async ({message}, {onSuccess, onUpdate}) => {
    agentRequestLoading.value = true;
    abortController = new AbortController();
    try {
      // 构建历史消息列表
      const historyMessages = messages.value.map((msg) => {
        const role = msg.status === 'local' ? 'user' : 'assistant';
        return {
          role,
          content: msg.message
        };
      });

      // 将附件内容添加到请求中
      let attachmentText = '';
      if (attachedFiles.value.length > 0) {
        for (const file of attachedFiles.value) {
          const text = await convertAttachmentToText(file.originFileObj);
          attachmentText += text + '\n';
        }
      }

      // 将attachmentText附带到historyMessages中最后一条的content中
      if (historyMessages.length > 0 && attachmentText) {
        historyMessages[historyMessages.length - 1].content += '\n' + attachmentText;
      }

      // 构建完整的消息列表，包含历史消息、附件内容和当前消息
      const fullMessages = [
        ...historyMessages
      ];

      const response = await fetch(OLLAMA_API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          model: MODEL, // 替换为你使用的模型
          messages: fullMessages,
          stream: true
        }),
        signal: abortController.signal
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let partialLine = '';
      let fullResponse = '';
      let thinkContent = ''; // 用于存储 <think> 标签内的内容
      let inThink = false; // 标记是否处于 <think> 标签内

      while (true) {
        const {done, value} = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, {stream: true});
        const lines = (partialLine + chunk).split('\n');
        partialLine = lines.pop() || '';

        for (const line of lines) {
          if (line) {
            try {
              const data = JSON.parse(line);
              const textChunk = data.message.content;

              // 处理流式响应中的 <think> 和 </think> 标签
              let currentIndex = 0;
              if (currentIndex < textChunk.length) {
                if (inThink) {
                  const endIndex = textChunk.indexOf('</think>', currentIndex);
                  if (endIndex !== -1) {  // think结束行，带think结束标签</think>的行
                    thinkContent += textChunk.slice(0, endIndex);
                    fullResponse += textChunk.slice(0, endIndex) + `</blockquote>`; // 思考内容结束
                    thinkContent = '';
                    inThink = false;
                    // console.log(`4, inThink: ${inThink}, textChunk： ${textChunk}, fullResponse: ${fullResponse}`);

                  } else {  // 两个 think 标签之间的内容
                    thinkContent += textChunk;
                    fullResponse += textChunk;
                    // console.log(`3, inThink: ${inThink}, textChunk： ${textChunk}, fullResponse: ${fullResponse}`);
                  }

                } else {  // 第1次进入 或 输出正文内容时

                  const startIndex = textChunk.indexOf('<think>');
                  if (startIndex !== -1) {  // 思考内容开始行，带think开始标签<think>的行
                    thinkContent += textChunk.slice(7);
                    fullResponse += `<blockquote>` + textChunk.slice(7);
                    inThink = true;
                    // console.log(`1, inThink: ${inThink}, textChunk： ${textChunk}, fullResponse: ${fullResponse}`);

                  } else {  // 正文内容
                    fullResponse += textChunk;
                    // console.log(`2, inThink: ${inThink}, textChunk： ${textChunk}`);
                  }
                }
              }

              const htmlChunk = md.render(fullResponse);
              onUpdate(htmlChunk);
            } catch (parseError) {
              console.error('解析流式响应出错:', parseError);
            }
          }
        }
      }

      const fullHtml = md.render(fullResponse);
      onSuccess(fullHtml);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('请求已被中断');
      } else {
        console.error('请求出错:', error);
        onSuccess('请求出错，请稍后再试。');
      }
    } finally {
      agentRequestLoading.value = false;
    }
  },
});

const {onRequest, messages, setMessages} = useXChat({
  agent: agent.value,
});

watch(activeKey, () => {
  if (activeKey.value !== undefined) {
    abortController.abort();
    setMessages([]);
  }
}, {immediate: true});

// ==================== Event ====================
const onSubmit = (nextContent: string) => {
  if (!nextContent) return;
  onRequest(nextContent);
  content.value = '';
  // 清空已选择的附件
  attachedFiles.value = [];
  // 收起附件选择框
  headerOpen.value = false;
};

// 停止输出
const onCancel = () => {
  abortController.abort(); // 中断请求
  agentRequestLoading.value = false; // 也可以在这里设置 loading 状态
};

const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
  onRequest(info.data.description as string);
};

const onAddConversation = () => {
  conversationsItems.value = [
    ...conversationsItems.value,
    {
      key: `${conversationsItems.value.length}`,
      label: `New Conversation ${conversationsItems.value.length}`,
    },
  ];
  activeKey.value = `${conversationsItems.value.length}`;
};

const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
  activeKey.value = key;
};

const handleFileChange: AttachmentsProps['onChange'] = (info) =>
    attachedFiles.value = info.fileList;

// ==================== Nodes ====================
const placeholderNode = computed(() => (
    <Space direction="vertical" size={16} style={styles.value.placeholder}>
      <Welcome
          variant="borderless"
          icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
          title="Hello, I'm Ant Design X"
          description="Base on Ant Design, AGI product interface solution, create a better intelligent vision~"
          extra={
            <Space>
              <Button icon={<ShareAltOutlined/>}/>
              <Button icon={<EllipsisOutlined/>}/>
            </Space>
          }
      />
      <Prompts
          title="Do you want?"
          items={placeholderPromptsItems}
          styles={{
            list: {
              width: '100%',
            },
            item: {
              flex: 1,
            },
          }}
          onItemClick={onPromptsItemClick}
      />
    </Space>
));

const items = computed<BubbleListProps['items']>(() => messages.value.map(({id, message, status}) => ({
  key: id,
  // 这里如果将loading设置为true，则气泡会显示加载状态，没有流式响应效果
  loading: false, // status === 'loading',
  role: status === 'local' ? 'local' : 'ai',
  // 直接渲染处理后的内容
  content: <div v-html={message}></div>
})));

const attachmentsNode = computed(() => (
    <Badge dot={attachedFiles.value.length > 0 && !headerOpen.value}>
      <Button type="text" icon={<PaperClipOutlined/>} onClick={() => headerOpen.value = !headerOpen.value}/>
    </Badge>
));

const senderHeader = computed(() => (
    <Sender.Header
        title="Attachments"
        open={headerOpen.value}
        onOpenChange={(open) => headerOpen.value = open}
        styles={{
          content: {
            padding: 0,
          },
        }}
    >
      <Attachments
          beforeUpload={() => false}
          items={attachedFiles.value}
          onChange={handleFileChange}
          placeholder={(type) =>
              type === 'drop'
                  ? {title: 'Drop file here'}
                  : {
                    icon: <CloudUploadOutlined/>,
                    title: 'Upload files',
                    description: 'Click or drag files to this area to upload',
                  }
          }
      />
    </Sender.Header>
));

/*const logoNode = computed(() => (
  <div style={styles.value.logo}>
    <img
      src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
      draggable={false}
      alt="logo"
      style={styles.value['logo-img']}
    />
    <span style={styles.value['logo-span']}>Ant Design X Vue</span>
  </div>
));*/

defineRender(() => {
  return (
      <div style={styles.value.layout}>
        <div style={styles.value.menu}>
          {/* ?? 添加会话 */}
          <Button
              onClick={onAddConversation}
              type="link"
              style={styles.value.addBtn}
              icon={<PlusOutlined/>}
          >
            新对话
          </Button>
          {/* ?? 会话管理 */}
          <Conversations
              items={conversationsItems.value}
              style={styles.value.conversations}
              activeKey={activeKey.value}
              onActiveChange={onConversationClick}
          />
        </div>
        <div style={styles.value.chat}>
          {/* ?? 消息列表 */}
          <Bubble.List
              items={items.value.length > 0 ? items.value : [{content: placeholderNode.value, variant: 'borderless'}]}
              roles={roles}
              style={styles.value.messages}
          />
          {/* ?? 提示词 */}
          <Prompts style={{color: token.value.colorText}} items={senderPromptsItems} onItemClick={onPromptsItemClick}/>
          {/* ?? 输入框 */}
          <Sender
              allowSpeech
              value={content.value}
              header={senderHeader.value}
              onSubmit={onSubmit}
              onChange={(value) => content.value = value}
              prefix={attachmentsNode.value}
              loading={agentRequestLoading.value}
              style={styles.value.sender}
              onCancel={onCancel}
          />
        </div>
      </div>
  )
});
</script>


<style scoped>
table {
  border-collapse: collapse;
  width: 100%;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

th {
  background-color: #f2f2f2;
}

/* 引用块的样式 */
:deep(blockquote) {
  display: block;
  padding-left: 16px;
  padding-right: 16px;
  margin: 0 0 24px;
  border-left: 3px solid #dddfe4;
  /*background-color: #eef0f4;*/
  overflow: auto;
  word-break: normal;
  color: #aaa;
}

</style>